/* 
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;

using DCSharp.Backend.Objects;
using DCSharp.Hashing;
using DCSharp.Logging;
using DCSharp.Settings;
using DCSharp.Xml;

namespace DCSharp.Backend.Managers
{
	public class ShareInfo
	{
		private string path;
		private string virtualName;

		public ShareInfo(string path, string virtualName)
		{
			this.path = path;
			this.virtualName = virtualName;
		}

		public ShareInfo()
		{
		}

		[XmlText]
		public string Path
		{
			get { return path; }
			set { path = value; }
		}

		[XmlAttribute]
		public string VirtualName
		{
			get { return virtualName; }
			set { virtualName = value; }
		}
	}

	public class DirectoryEventArgs : EventArgs
	{
		private string path;
		private string virtualName;

		public DirectoryEventArgs(string path, string virtualName)
		{
			this.path = path;
			this.virtualName = virtualName;
		}

		public string Path
		{
			get { return path; }
		}

		public string VirtualName
		{
			get { return virtualName; }
		}
	}

	public class DirectoryRenamedEventArgs : DirectoryEventArgs
	{
		private string oldName;

		public DirectoryRenamedEventArgs(string path, string oldName,
			string newName) : base(path, newName)
		{
			this.oldName = oldName;
		}

		public string OldVirtualName
		{
			get { return oldName; }
		}
	}

	/// <summary>
	/// Manages the shared files and directories.
	/// </summary>
	public class ShareManager : IEnumerable<ShareInfo>
	{
		public event EventHandler<DirectoryEventArgs> DirectoryAdded;
		public event EventHandler<DirectoryEventArgs> DirectoryRemoved;
		public event EventHandler<DirectoryRenamedEventArgs> DirectoryRenamed;

		//private static Logger log = LogManager.GetLogger("ShareManager");

		private Dictionary<string, string> sharedDirectories;
		private FileList fileList;
		private string xmlListPath;
		private long total;

		private HashStore hashStore;
		private HashManager hashManager;
		private Dictionary<FileNode, string> hashPending;

		private ShareLoader loader;
		private Dictionary<string, DirectoryNode> pending;

		private bool dirty;
		private bool listDirty;
		private object fileLock;

		/// <summary>
		/// Constructs a new instance using the specified hash store.
		/// </summary>
		/// <param name="settings"></param>
		/// <param name="hashStore">The <see cref="HashStore"/> to use.</param>
		/// <param name="dataDir">The directory where the file list is to be saved.</param>
		public ShareManager(IRuntimeSettings settings, HashStore hashStore,
			string dataDir, IEnumerable<ShareInfo> shared)
		{
			this.hashStore = hashStore;

			loader = new ShareLoader(settings, hashStore, Hash);
			loader.DirectoryLoaded += OnDirectoryLoaded;
			pending = new Dictionary<string, DirectoryNode>();

			sharedDirectories = new Dictionary<string, string>();
			fileList = new FileList(settings.LocalIdentity.Cid);
			xmlListPath = Path.Combine(dataDir, "files.xml");

			fileLock = new Object();

			// Hashing
			hashManager = new HashManager();
			hashManager.FileHashed += OnFileHashed;

			hashPending = new Dictionary<FileNode, string>();

			// Initial shared directories
			if (shared != null)
			{
				Load(shared);
			}
		}

		#region Properties

		/// <summary>
		/// Gets the number of bytes that remains until all files have been hashed.
		/// </summary>
		/// <value>The number of bytes that remains until all files have been hashed.</value>
		public long BytesRemaining
		{
			get { return hashManager.BytesRemaining; }
		}

		/// <summary>
		/// Gets whether or not the directories are being loaded.
		/// </summary>
		/// <value>Whether or not the directories are being loaded.</value>
		public bool IsLoading
		{
			get { return loader.IsLoading; }
		}

		/// <summary>
		/// Gets the total size of the shared directories in bytes.
		/// </summary>
		/// <value>The total size of the shared directories in bytes.</value>
		public long Total
		{
			get
			{
				if (dirty)
				{
					lock (sharedDirectories)
					{
						total = fileList.GetSize();
						dirty = false;
					}
				}
				return total;
			}
		}

		/// <summary>
		/// Gets or sets the path to the file list.
		/// </summary>
		/// <value>The path to the file list.</value>
		public string FileListPath
		{
			get { return xmlListPath; }
			set { xmlListPath = value; }
		}

		/// <summary>
		/// Gets or sets the path to the bzip2 compressed file list.
		/// </summary>
		/// <value>The path to the bzip2 compressed file list.</value>
		public string CompressedFileListPath
		{
			get { return xmlListPath + ".bz2"; }
		}

		public object SyncRoot
		{
			get { return sharedDirectories; }
		}

		protected FileList FileList
		{
			get { return fileList; }
		}

		#endregion

		#region Methods

		/// <summary>
		/// Adds a directory to the share.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		/// <param name="virtualName">The virtual name of the directory.</param>
		public void AddDirectory(string path, string virtualName)
		{
			if (path == null)
			{
				throw new ArgumentNullException("path");
			}
			if (virtualName == null)
			{
				throw new ArgumentNullException("virtualName");
			}
			lock (sharedDirectories)
			{
				if (sharedDirectories.ContainsKey(path))
				{
					throw new ArgumentException("The directory already exists",
						"path");
				}
				if (sharedDirectories.ContainsValue(virtualName))
				{
					throw new ArgumentException("A directory with that virtual name already exists",
						"virtualName");
				}
				sharedDirectories.Add(path, virtualName);
				Update(path);
			}
			OnDirectoryAdded(path, virtualName);
		}

		/// <summary>
		/// Removes a directory from the share.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		public void RemoveDirectory(string path)
		{
			if (path == null)
			{
				throw new ArgumentNullException("path");
			}

			string virtualName;
			lock (sharedDirectories)
			{
				if (!sharedDirectories.ContainsKey(path))
				{
					return;
				}
				virtualName = sharedDirectories[path];

				Abort(path);

				DirectoryNode directory = fileList[virtualName];
				if (directory != null)
				{
					fileList.Remove(directory);
					dirty = listDirty = true;
				}
				sharedDirectories.Remove(path);
			}
			OnDirectoryRemoved(path, virtualName);
		}

		/// <summary>
		/// Renames a directory.
		/// </summary>
		/// <param name="oldName">The old virtual name of the directory.</param>
		/// <param name="newName">The new virtual name.</param>
		/// <returns>True if the directory was successfully renamed, otherwise, false.</returns>
		public bool RenameDirectory(string oldName, string newName)
		{
			string path;
			lock (sharedDirectories)
			{
				if (!sharedDirectories.ContainsValue(oldName) ||
					sharedDirectories.ContainsValue(newName))
				{
					return false;
				}
				path = GetPath(oldName);
				sharedDirectories[path] = newName;

				DirectoryNode directory = fileList[oldName];
				if (directory != null)
				{
					directory.Name = newName;
				}
				if (pending.TryGetValue(path, out directory))
				{
					directory.Name = newName;
				}
			}
			OnDirectoryRenamed(path, oldName, newName);
			return true;
		}

		

		/// <summary>
		/// Refreshes the list of shared files.
		/// </summary>
		public void Refresh()
		{
			lock (sharedDirectories)
			{
				// Abort the loading
				foreach (string path in sharedDirectories.Keys)
				{
					loader.Abort(path);
				}
				pending.Clear();

				// Abort the hashing of all files.
				lock (hashStore)
				{
					foreach (KeyValuePair<FileNode, string> pair in hashPending)
					{
						hashManager.AbortHash(pair.Value);
					}
					hashPending.Clear();
				}

				// Update the directories
				foreach (string path in sharedDirectories.Keys)
				{
					Update(path);
				}
			}
		}

		/// <summary>
		/// Checks if the share contains a specific directory.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		/// <returns>True if the share contains the directory, otherwise, false.</returns>
		public bool ContainsDirectory(string path)
		{
			return sharedDirectories.ContainsKey(path);
		}

		/// <summary>
		/// Checks if the share contains a directory with a specific name.
		/// </summary>
		/// <param name="virtualName">The name to check.</param>
		/// <returns>True if the share contains a directory with the name, otherwise, false.</returns>
		public bool ContainsVirtualName(string virtualName)
		{
			return sharedDirectories.ContainsValue(virtualName);
		}

		public IEnumerator<ShareInfo> GetEnumerator()
		{
			foreach (KeyValuePair<string, string> pair in sharedDirectories)
			{
				yield return new ShareInfo(pair.Key, pair.Value);
			}
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		/// <summary>
		/// Gets the virtual name of a directory.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		/// <returns>The name of the directory, otherwise, null.</returns>
		public string GetVirtualName(string path)
		{
			string virtualName = null;
			sharedDirectories.TryGetValue(path, out virtualName);

			return virtualName;
		}

		/// <summary>
		/// Saves the file list to disk.
		/// </summary>
		/// <returns>The path to the file list.</returns>
		public void SaveFileList()
		{
			lock (sharedDirectories)
			{
				if (!listDirty)
				{
					FileInfo fileInfo = new FileInfo(xmlListPath);
					if (fileInfo.Exists)
					{
						// List not dirty, and the file exists.
						return;
					}
				}
				fileList.Save(xmlListPath);
				listDirty = false;
			}
			lock (fileLock)
			{
				FileUtil.Compress(xmlListPath, CompressedFileListPath,
					FileUtil.Compression.BZip2);
			}
		}

		/// <summary>
		/// Gets the hash tree with a specific root hash. 
		/// </summary>
		/// <param name="root">The Base32 encoded root hash.</param>
		/// <returns>The hash tree if found; otherwise, null.</returns> 
		public HashTree GetHashTree(string root)
		{
			lock (hashStore)
			{
				return hashStore.GetHashTree(root);
			}
		}

		#region GetPath methods

		/// <summary>
		/// Gets the path of the directory with the specified virtual name. 
		/// </summary>
		/// <param name="virtualName">The virtual name of the directory.</param>
		/// <returns>The path to the directory with the specified virtual name.</returns> 
		public string GetPath(string virtualName)
		{
			lock (sharedDirectories)
			{
				foreach (KeyValuePair<string, string> pair in sharedDirectories)
				{
					if (pair.Value == virtualName)
					{
						return pair.Key;
					}
				}
				return null;
			}
		}

		/// <remarks>
		/// The path should start with the virtual name, not a separator character.
		/// </remarks>
		/// <param name="virtualPath">The virtual path.</param>
		public string GetFullPath(string virtualPath)
		{
			// FIXME: Should check if the file exists in the file list 
			int slash = virtualPath.IndexOf(Path.DirectorySeparatorChar);
			if (slash > 0)
			{
				string virtualName = virtualPath.Substring(0, slash);
				string path = GetPath(virtualName);

				if (path != null)
				{
					// Removes the root from the second path to make Path.Combine
					// work as intended.
					return Path.Combine(path, virtualPath.Substring(slash + 1));
				}
			}
			return null;
		}

		public string GetFullPathFromTTH(string tth)
		{
			string path = GetPathFromTTH(tth);
			return path != null ? GetFullPath(path) : null;
		}

		public string GetPathFromTTH(string tth)
		{
			DirectoryNode[] directories;
			lock (sharedDirectories)
			{
				directories = fileList.Directories.ToArray();
			}

			foreach (DirectoryNode directory in directories)
			{
				string path = GetPathFromTTH(directory, tth, String.Empty);
				if (path != null)
				{
					return path;
				}
			}
			return null;
		}

		private static string GetPathFromTTH(DirectoryNode directory, string tth,
			string parent)
		{
			string currentPath = Path.Combine(parent, directory.Name);
			foreach(FileNode file in directory.Files)
			{
				if (file.TTH == tth)
				{
					return Path.Combine(currentPath, file.Name);
				}
			}

			foreach(DirectoryNode dir in directory.Directories)
			{
				string path = GetPathFromTTH(dir, tth, currentPath);
				if (path != null)
				{
					return path;
				}
			}
			return null;
		}

		#endregion

		#region Search

		/// <summary>
		/// Does a search in the shared directories for matching directories and
		/// files.
		/// </summary>
		/// <param name="searchInfo">The search criteria.</param>
		/// <param name="max">The max number of results to add.</param>
		public SearchResult[] Search(SearchInfo searchInfo, int? max)
		{
			DirectoryNode[] directories;
			lock (sharedDirectories)
			{
				directories = fileList.Directories.ToArray();
			}

			List<SearchResult> results = new List<SearchResult>();
			foreach (DirectoryNode directory in directories)
			{
				SearchDirectory(directory, searchInfo, String.Empty, results, max);
			}
			return results.ToArray();
		}

		private static void SearchDirectory(DirectoryNode directory,
			SearchInfo searchInfo, string parent, List<SearchResult> results,
			int? max)
		{
			string currentPath = Path.Combine(parent, directory.Name);
			foreach(DirectoryNode dir in directory.Directories)
			{
				SearchDirectory(dir, searchInfo, currentPath, results, max);
				if (max != null && results.Count >= max)
				{
					return;
				}
			}

			long? maxSize = searchInfo.MaxSize;
			long? minSize = searchInfo.MinSize;

			if ((searchInfo.Type == SearchFileType.Any ||
				searchInfo.Type == SearchFileType.Directory) &&
				searchInfo.TTH == null)
			{
				if ((maxSize == null || directory.GetSize() < maxSize) &&
					(minSize == null || directory.GetSize() > minSize))
				{
					if (StringUtil.MatchesKeywords(directory.Name,
						searchInfo.Keywords))
					{
						results.Add(new SearchResult(ResultType.Directory,
							currentPath));
					}
				}
			}

			// TODO: Check if the type matches
			if (searchInfo.Type != SearchFileType.Directory)
			{
				foreach(FileNode file in directory.Files)
				{
					if (max != null && results.Count >= max)
					{
						return;
					}
					if ((searchInfo.TTH != null && file.TTH != searchInfo.TTH) ||
						(maxSize != null && file.Size > maxSize) ||
						(minSize != null && file.Size < minSize))
					{
						continue;
					}
					if ((searchInfo.TTH != null && file.TTH == searchInfo.TTH) ||
						StringUtil.MatchesKeywords(file.Name, searchInfo.Keywords))
					{
						string path = Path.Combine(currentPath, file.Name);
						results.Add(new SearchResult(ResultType.File, path,
							file.Size, file.TTH));
					}
				}
			}
		}

		#endregion

		/// <summary>
		/// Emits the DirectoryAdded event.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		/// <param name="name">The name of the directory.</param>
		protected virtual void OnDirectoryAdded(string path, string name)
		{
			if (DirectoryAdded != null)
			{
				DirectoryAdded(this, new DirectoryEventArgs(path, name));
			}
		}

		/// <summary>
		/// Emits the DirectoryRemoved event.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		/// <param name="name">The name of the directory.</param> 
		protected virtual void OnDirectoryRemoved(string path, string name)
		{
			if (DirectoryRemoved != null)
			{
				DirectoryRemoved(this, new DirectoryEventArgs(path, name));
			}
		}

		/// <summary>
		/// Emits the DirectoryRenamed event.
		/// </summary>
		/// <param name="path">The path to the directory.</param>
		/// <param name="oldName">The old virtual name.</param>
		/// <param name="newName">The new virtual name.</param>
		protected virtual void OnDirectoryRenamed(string path, string oldName,
			string newName)
		{
			if (DirectoryRenamed != null)
			{
				DirectoryRenamed(this, new DirectoryRenamedEventArgs(path,
					oldName, newName));
			}
		}

		private void Update(string path)
		{
			Abort(path);

			DirectoryNode directory = new DirectoryNode(sharedDirectories[path]);
			pending.Add(path, directory);

			loader.LoadDirectory(path, directory);
		}

		private void Abort(string path)
		{
			loader.Abort(path);

			DirectoryNode oldDir = fileList[sharedDirectories[path]];
			DirectoryNode newDir;

			if (pending.TryGetValue(path, out newDir) ||
				oldDir != null)
			{
				lock (hashStore)
				{
					AbortHashing(newDir ?? oldDir);
				}
				pending.Remove(path);
			}
		}

		private void Hash(FileNode file, FileInfo fileInfo)
		{
			lock (hashStore)
			{
				hashPending.Add(file, fileInfo.FullName);
				hashManager.Hash(fileInfo);
			}
		}

		private void AbortHashing(DirectoryNode directory)
		{
			foreach (FileNode file in directory.Files)
			{
				string filename;
				if (hashPending.TryGetValue(file, out filename))
				{
					hashManager.AbortHash(filename);
					hashPending.Remove(file);
				}
			}
			directory.Directories.ForEach(AbortHashing);
		}

		private void Load(IEnumerable<ShareInfo> shared)
		{
			FileList tempList = new FileList();
			try
			{
				if (File.Exists(xmlListPath))
				{
					tempList.Load(xmlListPath);
				}
			}
			catch
			{
			}

			// Update from the cached list and infos.
			foreach (ShareInfo info in shared)
			{
				if (!sharedDirectories.ContainsKey(info.Path) &&
					!sharedDirectories.ContainsValue(info.VirtualName))
				{
					sharedDirectories.Add(info.Path, info.VirtualName);

					DirectoryNode node = tempList[info.VirtualName];
					if (node != null)
					{
						fileList.Add(node);
					}
					Update(info.Path);
				}
			}
			dirty = true;
		}

		private void OnDirectoryLoaded(object obj, DirectoryLoadedEventArgs args)
		{
			lock (sharedDirectories)
			{
				if (!sharedDirectories.ContainsKey(args.Path) ||
					!pending.ContainsKey(args.Path))
				{
					return;
				}

				DirectoryNode oldDir = fileList[args.Directory.Name];
				if (oldDir != null)
				{
					fileList.Remove(oldDir);
				}

				DirectoryNode directory = pending[args.Path];
				pending.Remove(args.Path);

				fileList.Add(directory);
				dirty = listDirty = true;
			}
		}

		private void OnFileHashed(object obj, HashEventArgs args)
		{
			lock (hashStore)
			{
				FileNode file = null;
				foreach (KeyValuePair<FileNode, string> pair in hashPending)
				{
					if (pair.Value == args.FileInfo.FullName)
					{
						file = pair.Key;
						break;
					}
				}

				if (file != null)
				{
					hashStore.AddFile(args.FileInfo, args.HashTree);
					file.TTH = Base32.Encode(args.HashTree.Root);

					hashPending.Remove(file);
				}
			}
		}

		#endregion
	}
}
